from app.data.resources.resources import RESOURCES
from app.data.database.database import DB

from app.engine.combat.solver import CombatPhaseSolver

from app.engine.sound import get_sound_thread
from app.engine import engine, combat_calcs, gui, action, item_system, skill_system
from app.engine.health_bar import MapCombatInfo
from app.engine.animations import MapAnimation
from app.engine.game_state import game

from app.engine.combat.simple_combat import SimpleCombat


class MapCombat(SimpleCombat):
    alerts: bool = True

    def __init__(self, attacker, main_item, items, positions, main_target_positions, splash_positions, script, total_rounds=1):
        self._full_setup(attacker, main_item, items, positions,
                         main_target_positions, splash_positions)
        self.state_machine = CombatPhaseSolver(
            attacker, self.main_item, self.items,
            self.defenders, self.splashes, self.target_positions,
            self.defender, self.def_item, script, total_rounds)

        self.last_update = engine.get_time()
        self.state = 'init'
        self.hp_bar_time = 400

        self._skip = False
        self.full_playback = []
        self.playback = []
        self.actions = []

        self.animations = []
        self.health_bars = {}

        self.first_phase = True
        self.cast_pose = False

    def set_state(self, state: str):
        self.state = state

    def skip(self):
        self._skip = True
        self.attacker.sprite.reset()
        if self.attacker.strike_partner:
            self.attacker.strike_partner.sprite.reset()
        if self.defender:
            self.defender.sprite.reset()
            if self.defender.strike_partner:
                self.defender.strike_partner.sprite.reset()

    def update(self) -> bool:
        current_time = engine.get_time() - self.last_update
        current_state = self.state
        # Only for the very first phase
        if self.state == 'init':
            game.highlight.remove_highlights()
            self.start_combat()
            self.start_event()
            self.set_state('init_pause')

        elif self.state == 'init_pause':
            if self._skip or current_time > 200:
                self.set_state('begin_phase')

        # print("Map Combat %s" % self.state)
        elif self.state == 'begin_phase':
            # Get playback
            if not self.state_machine.get_state():
                self.clean_up0()
                self.set_state('exp_wait')
                return False

            self.actions, self.playback = self.state_machine.do()
            self.full_playback += self.playback
            if not self.actions and not self.playback:
                self.state_machine.setup_next_state()
                return False
            if not item_system.no_map_hp_display(self.attacker, self.main_item):
                self._build_health_bars()
            if self.first_phase:
                self.set_up_pre_proc_animation('attack_pre_proc')
                self.set_up_pre_proc_animation('defense_pre_proc')
                self.set_up_other_proc_icons()
                self.first_phase = False
            self.add_proc_icon.memory.clear()

            # Camera
            if self.get_from_playback('defender_phase') or self.get_from_playback('defender_partner_phase'):
                game.cursor.set_pos(self.attacker.position)
            else:
                if self.defender:
                    game.cursor.set_pos(self.defender.position)
                elif self.target_positions[0]:
                    game.cursor.set_pos(self.target_positions[0])
            if not self._skip:
                game.state.change('move_camera')

            # Sprites
            if self.get_from_playback('defender_phase') or self.get_from_playback('defender_partner_phase'):
                if self.defender:
                    if self.get_from_playback('defender_phase'):
                        self.defender.sprite.change_state('combat_attacker')
                    elif self.get_from_playback('defender_partner_phase'):
                        self.defender.strike_partner.sprite.change_state(
                            'combat_attacker')
                self.attacker.sprite.change_state('combat_counter')
                if self.attacker.strike_partner:
                    self.attacker.strike_partner.sprite.change_state(
                        'combat_counter')
            else:
                if self.get_from_playback('defender_partner_phase'):
                    self.attacker.strike_partner.sprite.change_state(
                        'combat_attacker')
                else:
                    self.attacker.sprite.change_state('combat_attacker')
                    if self.attacker.strike_partner:
                        self.attacker.strike_partner.sprite.change_state(
                            'combat_counter')
                if self.defender:
                    self.defender.sprite.change_state('combat_defender')
                    if self.defender.strike_partner:
                        self.defender.strike_partner.sprite.change_state(
                            'combat_defender')

            # handle cast pose 
            if self.defender and self.defender.sprite.state == 'combat_attacker' and \
                    self.def_item and item_system.map_cast_pose(self.defender, self.def_item) and\
                    self.get_from_playback('defender_phase'):
                self.defender.sprite.change_state("start_cast")
                self.cast_pose = True
            elif self.defender and self.defender.strike_partner and \
                    self.defender.strike_partner.sprite.state == 'combat_attacker' and \
                    self.defense_partner_weapon and \
                    item_system.map_cast_pose(self.defender.strike_partner, self.defense_partner_weapon) and \
                    self.get_from_playback('defender_partner_phase'):
                self.defender.strike_partner.sprite.change_state(
                    "start_cast")
                self.cast_pose = True
            elif self.attacker.strike_partner and self.attack_partner_weapon and \
                    item_system.map_cast_pose(self.attacker.strike_partner, self.attack_partner_weapon) and \
                    self.get_from_playback('attacker_partner_phase'):
                self.attacker.strike_partner.sprite.change_state(
                    "start_cast")
                self.cast_pose = True
            elif self.attacker.position and self.main_item and \
                    not self.get_from_playback('attacker_partner_phase') and \
                    item_system.map_cast_pose(self.attacker, self.main_item):
                self.attacker.sprite.change_state("start_cast")
                self.cast_pose = True
            
            self.set_state('proc_animations')

        elif self.state == 'proc_animations':
            # Handle proc effects
            self.set_up_proc_animation('attack_proc')
            self.set_up_proc_animation('attack_hit_proc')
            self.set_up_proc_animation('defense_proc')
            self.set_up_proc_animation('defense_hit_proc')

            self.set_state('start_anim')

        elif self.state == 'start_anim':
            if self._skip or current_time > 200:
                animation_brushes = self.get_from_playback('cast_anim')
                for brush in animation_brushes:
                    anim = RESOURCES.animations.get(brush.anim)
                    pos = game.cursor.position
                    if anim:
                        anim = MapAnimation(anim, pos)
                        self.animations.append(anim)

                animation_brushes = self.get_from_playback('target_cast_anim')
                for brush in animation_brushes:
                    anim = RESOURCES.animations.get(brush.anim)
                    if anim:
                        anim = MapAnimation(anim, brush.pos)
                        self.animations.append(anim)
                self.set_state('sound')

        elif self.state == 'sound':
            if self._skip or current_time > 250:
                if self.defender and self.defender.sprite.state == 'combat_attacker' and \
                        self.get_from_playback('defender_phase'):
                    self.defender.sprite.change_state("combat_anim")
                elif self.defender and self.defender.strike_partner and \
                        self.defender.strike_partner.sprite.state == 'combat_attacker' and \
                        self.get_from_playback('defender_partner_phase'):
                    self.defender.strike_partner.sprite.change_state(
                        "combat_anim")
                elif self.get_from_playback('attacker_partner_phase') and \
                        not self.attacker.strike_partner.sprite.state == 'start_cast':
                    self.attacker.strike_partner.sprite.change_state(
                        "combat_anim")
                elif self.attacker.position and not self.attacker.sprite.state == 'start_cast' and \
                        not self.get_from_playback('attacker_partner_phase'):
                    self.attacker.sprite.change_state("combat_anim")
                sound_brushes = self.get_from_playback('cast_sound')
                for brush in sound_brushes:
                    get_sound_thread().play_sfx(brush.sound)
                
                self.set_state('anim')

        elif self.state == 'anim':
            if self._skip or current_time > 83:
                self._handle_playback()
                self._apply_actions()

                # Force update hp bars so we can get timing info
                for hp_bar in self.health_bars.values():
                    hp_bar.update()
                if self.health_bars:
                    self.hp_bar_time = max(hp_bar.get_time_for_change()
                                           for hp_bar in self.health_bars.values())
                else:
                    self.hp_bar_time = 0
                self.set_state('hp_bar_wait')

        elif self.state == 'hp_bar_wait':
            if self._skip or current_time > self.hp_bar_time:
                self.set_state('end_phase_anim')

        elif self.state == 'end_phase_anim':
            if self._skip or (self.cast_pose and current_time > 250) or current_time > 550:
                if self.defender and self.defender.sprite.state == 'combat_anim':
                    self.defender.sprite.change_state('combat_attacker')
                elif self.defender and self.defender.sprite.state == 'start_cast':
                    self.defender.sprite.change_state('end_cast')
                elif self.defender and self.defender.strike_partner and \
                        self.defender.strike_partner.sprite.state == 'combat_anim':
                    self.defender.strike_partner.sprite.change_state(
                        'combat_attacker')
                elif self.defender and self.defender.strike_partner and \
                        self.defender.strike_partner.sprite.state == 'start_cast':
                    self.defender.strike_partner.sprite.change_state(
                        'end_cast')
                elif self.attacker.strike_partner and \
                        self.attacker.strike_partner.sprite.state == 'combat_anim':
                    self.attacker.strike_partner.sprite.change_state(
                        'combat_attacker')
                elif self.attacker.strike_partner and \
                        self.attacker.strike_partner.sprite.state == 'start_cast':
                    self.attacker.strike_partner.sprite.change_state(
                        'end_cast')
                elif self.attacker.position and self.attacker.sprite.state == 'combat_anim':
                    self.attacker.sprite.change_state('combat_attacker')
                elif self.attacker.position and self.attacker.sprite.state == 'start_cast':
                    self.attacker.sprite.change_state('end_cast')
                self.set_state('end_phase')
        
        elif self.state == 'end_phase':
            if self._skip or not self.cast_pose or (self.cast_pose and current_time > 550):
                self.cast_pose = False
                self._end_phase()
                self.state_machine.setup_next_state()
                self.set_state('begin_phase')
                
        elif self.state == 'exp_wait':
            self.clean_up1()
            self.set_state('post_combat')
            
        elif self.state == 'post_combat':
            self.clean_up2()
            return True

        if self.state != current_state:
            self.last_update = engine.get_time()

        if self.state not in ('begin_phase', 'proc_animations'):
            for hp_bar in self.health_bars.values():
                hp_bar.update()

        return False

    def _show_guard_gauge(self) -> bool:
        return DB.constants.value('pairup') and not DB.constants.value('attack_stance_only')

    def _build_health_bars(self):
        if (self.defender and self.all_splash) or len(self.all_splash) > 1:
            # Many splash attacks
            # No health bars!!
            self.health_bars.clear()

        else:
            # P1 on P1
            if self.defender and self.attacker is self.defender:
                hit = combat_calcs.compute_hit(
                    self.attacker, self.defender, self.main_item, self.def_item, 'attack', self.state_machine.get_attack_info())
                mt = combat_calcs.compute_damage(
                    self.attacker, self.defender, self.main_item, self.def_item, 'attack', self.state_machine.get_attack_info())
                crt = combat_calcs.compute_crit(
                    self.attacker, self.defender, self.main_item, self.def_item, 'attack', self.state_machine.get_attack_info())
                grd = self.attacker.get_guard_gauge() if self._show_guard_gauge() else None
                if self.attacker not in self.health_bars:
                    attacker_health = MapCombatInfo(
                        'p1', self.attacker, self.main_item, self.defender, (hit, mt, crt, grd))
                    self.health_bars[self.attacker] = attacker_health
                else:
                    self.health_bars[self.attacker].update_stats((hit, mt, crt, grd))

            # P1 on P2
            elif self.defender:
                # Attacker
                hit = combat_calcs.compute_hit(
                    self.attacker, self.defender, self.main_item, self.def_item, 'attack', self.state_machine.get_attack_info())
                mt = combat_calcs.compute_damage(
                    self.attacker, self.defender, self.main_item, self.def_item, 'attack', self.state_machine.get_attack_info())
                crt = combat_calcs.compute_crit(
                    self.attacker, self.defender, self.main_item, self.def_item, 'attack', self.state_machine.get_attack_info())
                grd = self.attacker.get_guard_gauge() if self._show_guard_gauge() else None
                if self.attacker not in self.health_bars:
                    attacker_health = MapCombatInfo(
                        'p1', self.attacker, self.main_item, self.defender, (hit, mt, crt, grd))
                    self.health_bars[self.attacker] = attacker_health
                else:
                    self.health_bars[self.attacker].update_stats((hit, mt, crt, grd))

                # Defender
                if combat_calcs.can_counterattack(self.attacker, self.main_item, self.defender, self.def_item):
                    hit = combat_calcs.compute_hit(
                        self.defender, self.attacker, self.def_item, self.main_item, 'defense', self.state_machine.get_defense_info())
                    mt = combat_calcs.compute_damage(
                        self.defender, self.attacker, self.def_item, self.main_item, 'defense', self.state_machine.get_defense_info())
                    crt = combat_calcs.compute_crit(
                        self.defender, self.attacker, self.def_item, self.main_item, 'defense', self.state_machine.get_defense_info())
                else:
                    hit, mt, crt = None, None, None
                grd = self.defender.get_guard_gauge() if self._show_guard_gauge() else None
                if self.defender not in self.health_bars:
                    defender_health = MapCombatInfo(
                        'p2', self.defender, self.def_item, self.attacker, (hit, mt, crt, grd))
                    self.health_bars[self.defender] = defender_health
                else:
                    self.health_bars[self.defender].update_stats((hit, mt, crt, grd))

            # P1 on single splash
            elif len(self.all_splash) == 1:
                defender = self.all_splash[0]
                hit = combat_calcs.compute_hit(
                    self.attacker, defender, self.main_item, None, 'attack', self.state_machine.get_attack_info())
                mt = combat_calcs.compute_damage(
                    self.attacker, defender, self.main_item, None, 'attack', self.state_machine.get_attack_info())
                crt = combat_calcs.compute_crit(
                    self.attacker, defender, self.main_item, None, 'attack', self.state_machine.get_attack_info())
                if self.attacker not in self.health_bars:
                    attacker_health = MapCombatInfo(
                        'p1', self.attacker, self.main_item, defender, (hit, mt, crt, None))
                    self.health_bars[self.attacker] = attacker_health
                else:
                    self.health_bars[self.attacker].update_stats((hit, mt, crt, None))

                if defender not in self.health_bars:
                    splash_health = MapCombatInfo(
                        'splash', defender, None, self.attacker, (None, None, None, None))
                    self.health_bars[defender] = splash_health

    def _handle_playback(self):
        for brush in self.playback:
            if brush.nid == 'unit_tint_add':
                brush.unit.sprite.begin_flicker(333, brush.color, 'add')
            elif brush.nid == 'unit_tint_sub':
                brush.unit.sprite.begin_flicker(333, brush.color, 'sub')
            elif brush.nid == 'crit_tint':
                brush.unit.sprite.begin_flicker(33, brush.color, 'add')
                # Delay five frames
                brush.unit.sprite.start_flicker(83, 33, brush.color, 'add')
                # Delay five more frames
                brush.unit.sprite.start_flicker(
                    166, 333, brush.color, 'add', fade_out=True)
            elif brush.nid == 'crit_vibrate':
                # In 10 frames, start vibrating for 12 frames
                brush.unit.sprite.start_vibrate(166, 200)
            elif brush.nid == 'hit_sound':
                get_sound_thread().play_sfx(brush.sound)
            elif brush.nid == 'shake':
                shake = brush.shake
                if self.get_from_playback('attack_proc'):
                    shake = 3  # Force critical type shake
                for health_bar in self.health_bars.values():
                    health_bar.shake(shake)
            elif brush.nid == 'hit_anim':
                anim = RESOURCES.animations.get(brush.anim)
                pos = brush.defender.position
                if anim and pos:
                    anim = MapAnimation(anim, pos)
                    self.animations.append(anim)
            elif brush.nid == 'damage_hit':
                damage = brush.damage
                if damage <= 0:
                    continue
                str_damage = str(damage)
                target = brush.defender
                for idx, num in enumerate(str_damage):
                    d = gui.DamageNumber(int(num), idx, len(
                        str_damage), target.position, 'small_red')
                    target.sprite.damage_numbers.append(d)
            elif brush.nid == 'damage_crit':
                damage = brush.damage
                if damage <= 0:
                    continue
                str_damage = str(damage)
                target = brush.defender
                for idx, num in enumerate(str_damage):
                    d = gui.DamageNumber(int(num), idx, len(
                        str_damage), target.position, 'small_yellow')
                    target.sprite.damage_numbers.append(d)
            elif brush.nid == 'heal_hit':
                damage = brush.damage
                if damage <= 0:
                    continue
                str_damage = str(damage)
                target = brush.defender
                for idx, num in enumerate(str_damage):
                    d = gui.DamageNumber(int(num), idx, len(
                        str_damage), target.position, 'small_cyan')
                    target.sprite.damage_numbers.append(d)

    def _apply_actions(self):
        """
        Actually commit the actions that we had stored!
        """
        for act in self.actions:
            action.do(act)

    def set_up_proc_animation(self, mark_type):
        marks = self.get_from_playback(mark_type)
        for mark in marks:
            self.add_proc_icon(mark.unit, mark.skill)

    def set_up_pre_proc_animation(self, mark_type):
        marks = self.get_from_full_playback(mark_type)
        for mark in marks:
            self.add_proc_icon(mark.unit, mark.skill)

    def set_up_other_proc_icons(self):
        for skill in self.attacker.skills:
            if skill_system.get_show_skill_icon(self.attacker, skill):
                self.add_proc_icon(self.attacker, skill)
        if self.defender:
            for skill in self.defender.skills:
                if skill_system.get_show_skill_icon(self.defender, skill):
                    self.add_proc_icon(self.defender, skill)

    def add_proc_icon(self, unit, skill):
        if unit in self.health_bars:
            # Check if we should be hiding this skill
            if skill_system.get_hide_skill_icon(unit, skill):
                return
            if skill.nid in self.add_proc_icon.memory.get(unit.nid, []):
                return

            health_bar = self.health_bars[unit]
            right = health_bar.ordering == 'right' or health_bar.ordering == 'middle'
            skill_icon = gui.SkillIcon(skill, right)
            health_bar.add_skill_icon(skill_icon)

            # Make sure the same proc icon never shows up twice in the same phase
            if unit.nid not in self.add_proc_icon.memory:
                self.add_proc_icon.memory[unit.nid] = []
            self.add_proc_icon.memory[unit.nid].append(skill.nid)
    add_proc_icon.memory = {}

    def _end_phase(self):
        pass

    def handle_support_pairs(self, pairs):
        for pair in pairs:
            unit1, unit2 = pair
            if not unit1.position or not unit2.position:
                continue
            unit1.sprite.add_animation('support_up', loop=False)
            unit2.sprite.add_animation('support_up', loop=False)

    def draw(self, surf):
        # Animations
        self.animations = [
            anim for anim in self.animations if not anim.update()]
        for anim in self.animations:
            anim.draw(surf, offset=(-game.camera.get_x(), -game.camera.get_y()))

        for hp_bar in self.health_bars.values():
            hp_bar.draw(surf)

        return surf
